﻿using FFmpeg.AutoGen;
using System;
using System.Collections.Generic;

namespace FFmpegLib;

public sealed unsafe class FAVFormatContext : IDisposable
{
    private static readonly AVRational TimeBaseQ = new AVRational { num = 1, den = ffmpeg.AV_TIME_BASE };

    static FAVFormatContext()
    {
        FFmpegLib.FFmpegUtils.FFmpegLibInit();
    }

    public string ErrInf { get; set; }

    public int VideoIndex { get; private set; } = -1;
    public int AudioIndex { get; private set; } = -1;
    public bool FmtCtxIsOpen { get; private set; }
    public bool IsInput { get; private set; }

    public AVFormatContext* FmtCtx;
    private AVCodecContext* VideoDeocder;
    private AVCodecContext* AudioDeocder;

    public AVCodecID VideoCodecID => Streams.TryGetValue(AVMediaType.AVMEDIA_TYPE_VIDEO, out var stream) ? stream.CodecID : 0;
    public AVCodecID AudioCodecID => Streams.TryGetValue(AVMediaType.AVMEDIA_TYPE_AUDIO, out var stream) ? stream.CodecID : 0;

    public Dictionary<AVMediaType, FAVStream> Streams { get; private set; } = new Dictionary<AVMediaType, FAVStream>();
    public FAVStream AudioStream => Streams.TryGetValue(AVMediaType.AVMEDIA_TYPE_AUDIO, out var stream) ? stream : null;
    public FAVStream VideoStream => Streams.TryGetValue(AVMediaType.AVMEDIA_TYPE_VIDEO, out var stream) ? stream : null;
    public double TotalSecs { get; private set; } = -1;

    /// <summary>
    /// 视频流第一帧pts
    /// </summary>
    public long VideoFirstPts;

    public FAVFormatContext() { }

    public FAVFormatContext(AVFormatContext* fmtCtx)
    {
        FmtCtx = fmtCtx;
    }

    public FAVFormatContext(IntPtr ptr)
    {
        FmtCtx = (AVFormatContext*)ptr;
    }

    public bool OpenFmtCtx(string url)
    {
        if (string.IsNullOrEmpty(url))
            throw new ArgumentNullException($"{nameof(url)}不能为空");

        if (FmtCtxIsOpen) { return true; }

        AVFormatContext* fmt_ctx = FmtCtx;
        if (fmt_ctx == null)
            fmt_ctx = ffmpeg.avformat_alloc_context();

        int ret = ffmpeg.avformat_open_input(&fmt_ctx, url, null, null);
        if (ret != 0) { goto error; }

        ret = ffmpeg.avformat_find_stream_info(fmt_ctx, null);
        if (ret != 0) { goto error; }

        if (fmt_ctx->duration != ffmpeg.AV_NOPTS_VALUE)
        {
            AVRational avr = fmt_ctx->streams[0]->time_base;
            AVRational rate = fmt_ctx->streams[0]->avg_frame_rate;
            TotalSecs = 1d * fmt_ctx->streams[0]->duration * avr.num / avr.den;
        }

        for (int i = 0; i < fmt_ctx->nb_streams; i++)
        {
            AVStream* stream = fmt_ctx->streams[i];
            AVCodecParameters* codecParam = stream->codecpar;
            if (codecParam->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
            {
                VideoIndex = i;
                FAVStream videoStream = new FAVStream(stream, TotalSecs);
                VideoDeocder = videoStream.CodecCtx;
                Streams.TryAdd(AVMediaType.AVMEDIA_TYPE_VIDEO, videoStream);
            }
            else if (codecParam->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO)
            {
                AudioIndex = i;
                FAVStream audioStream = new FAVStream(stream, TotalSecs);
                AudioDeocder = audioStream.CodecCtx;
                Streams.TryAdd(AVMediaType.AVMEDIA_TYPE_AUDIO, audioStream);
            }
        }

        FmtCtx = fmt_ctx;
        IsInput = true;
        return FmtCtxIsOpen = true;

    error:
        ErrInf = FFmpegUtils.GetErrorMessage(ret); // throw new ApplicationException(GetErrorMessage(error));
        ffmpeg.avformat_free_context(fmt_ctx);
        return false;
    }

    public bool CreateOutputFmtCtx(string url, string formatName,
        AVCodecParameters* videoCodecParam = null,
        AVCodecParameters* audioCodecParam = null)
    {
        if (string.IsNullOrEmpty(url))
            throw new ArgumentNullException($"{nameof(url)}不能为空");

        if (FmtCtxIsOpen) { return true; }

        int ret = 0;
        AVFormatContext* fmt_ctx = null;
        if (fmt_ctx == null)
            ret = ffmpeg.avformat_alloc_output_context2(&fmt_ctx, null, formatName, url);

        ret = ffmpeg.avio_open2(&fmt_ctx->pb, url, ffmpeg.AVIO_FLAG_READ_WRITE, null, null);
        if (ret != 0) { goto error; }

        if (videoCodecParam != null)
        {
            AVCodec* curCodec = ffmpeg.avcodec_find_decoder(videoCodecParam->codec_id);
            AVStream* stream = ffmpeg.avformat_new_stream(fmt_ctx, curCodec);
            ret = ffmpeg.avcodec_parameters_copy(stream->codecpar, videoCodecParam);
            stream->time_base = new AVRational { num = 1, den = ffmpeg.AV_TIME_BASE };
        }
        if (audioCodecParam != null)
        {
            AVCodec* curCodec = ffmpeg.avcodec_find_decoder(audioCodecParam->codec_id);
            AVStream* stream = ffmpeg.avformat_new_stream(fmt_ctx, curCodec);
            ret = ffmpeg.avcodec_parameters_copy(stream->codecpar, audioCodecParam);
            stream->time_base = new AVRational { num = 1, den = ffmpeg.AV_TIME_BASE };
        }
        ret = ffmpeg.avformat_write_header(fmt_ctx, null);
        if (ret < 0)
        { goto error; }

        FmtCtxIsOpen = true;
        IsInput = false;
        return FmtCtxIsOpen = true;

    error:
        ErrInf = FFmpegUtils.GetErrorMessage(ret); // throw new ApplicationException(GetErrorMessage(error));
        ffmpeg.avformat_free_context(fmt_ctx);
        return false;
    }

    public int ReadPacket(FAVPacket packet)
    {
        if (packet == null)
            throw new ArgumentNullException($"argumentName({nameof(packet)}) is null,is not allowed");
        lock (this)
            return ffmpeg.av_read_frame(FmtCtx, packet.Ptr);
    }

    public int Decode(FAVPacket inPacket, FAVFrame outFrame)
    {
        AVCodecContext* curCtx = null;
        if (inPacket.StreamIndex == VideoIndex)
        {
            curCtx = VideoDeocder;
            outFrame.MediaType = AVMediaType.AVMEDIA_TYPE_VIDEO;
        }
        else if (inPacket.StreamIndex == AudioIndex)
        {
            curCtx = AudioDeocder;
            outFrame.MediaType = AVMediaType.AVMEDIA_TYPE_AUDIO;
        }
        return DecodeAVPacket(curCtx, inPacket.Ptr, outFrame.Ptr);
    }

    /// <summary>
    /// 跳转到指定时间
    /// </summary>
    /// <param name="ms">毫秒</param>
    /// <returns></returns>
    public bool Seek(double seconds)
    {
        if (IsDisposed)
        {
            return false;
        }
        bool boolRet = true;
        lock (this)
        {
            if (AudioStream != null)
            {
                AudioStream.CurMsPos = (long)(seconds * 1000);
                long seekPos = GetPosFromTime(AudioStream.TimeBase, seconds);
                int rs = ffmpeg.av_seek_frame(FmtCtx, AudioIndex, seekPos, ffmpeg.AVSEEK_FLAG_BACKWARD);
                boolRet &= rs >= 0;
            }
            if (VideoStream != null)
            {
                VideoStream.CurMsPos = (long)(seconds * 1000);
                long seekPos = GetPosFromTime(VideoStream.TimeBase, seconds);
                int rs = ffmpeg.av_seek_frame(FmtCtx, VideoIndex, seekPos, ffmpeg.AVSEEK_FLAG_BACKWARD);
                boolRet &= rs >= 0;
            }
        }
        return boolRet;
    }



    #region dispose
    public bool IsDisposed => disposedValue;

    private bool disposedValue;
    private void Destroy()
    {
        lock (this)
        {
            if (VideoDeocder != null)
            {
                ffmpeg.avcodec_close(VideoDeocder);
                fixed (AVCodecContext** ptr = &VideoDeocder)
                    ffmpeg.avcodec_free_context(ptr);
            }
            if (AudioDeocder != null)
            {
                ffmpeg.avcodec_close(AudioDeocder);
                fixed (AVCodecContext** ptr = &AudioDeocder)
                    ffmpeg.avcodec_free_context(ptr);
            }
            if (FmtCtx != null)
            {
                fixed (AVFormatContext** ptr = &FmtCtx)
                    ffmpeg.avformat_close_input(ptr);
                ffmpeg.avformat_free_context(FmtCtx);
                FmtCtx = null;
            }
        }
    }

    protected void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                Destroy();
            }
            disposedValue = true;
        }
    }

    public void Dispose()
    {
        // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
    #endregion

    #region static

    /// <summary>
    /// seek 时间转pts
    /// </summary>
    /// <param name="inStream"></param>
    /// <param name="seconds"></param>
    /// <returns></returns>
    public static long GetPosFromTime(AVRational inRational, double seconds)
    {
        long time = (long)(ffmpeg.AV_TIME_BASE * seconds);
        return ffmpeg.av_rescale_q(time, FAVFrame.TimeBaseQ, inRational);
    }

    public static int DecodeAVPacket(AVCodecContext* dec_ctx, AVPacket* in_packet, AVFrame* out_frame)
    {
        int ret = ffmpeg.avcodec_send_packet(dec_ctx, in_packet);
        if (ret < 0)
        {
            return ret;
        }
        // read all the output frames (in general there may be any number of them 
        return ffmpeg.avcodec_receive_frame(dec_ctx, out_frame);
    }
    #endregion
}
